<?php

namespace mongrove;

use \Closure;

/**
 *
 * The Field class represents a field in a Record and Structure. This class
 * is responsible for hydration from Mongo arrays, dehydration to Mongo arrays,
 * the collection of mutations for modified Fields and the rewriting of queries,
 * if applicable.
 *
 * @author Gido Hakvoort <gido@hakvoort.it>
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 *
 */
interface Field {

    /**
     * The field's state is clean
     *
     * @var integer
     */
    const STATE_CLEAN	= 0x00;

    /**
     * The mask bit for a new field
     *
     * @var integer
     */
    const STATE_NEW 	= 0x01;

    /**
     * The mask bit for a modified field
     *
     * @var integer
     */
    const STATE_DIRTY	= 0x02;

    /**
     *
     * Return the value which this Field represents. The represented value
     * does not need to be of the same type as the contained value. (e.g. a
     * MongoDate can be represented by an integer, a MongoRegex by a string, etc.)
     *
     * The returned value must be acceptable by setValue.
     *
     * @return mixed
     */
    public function getValue();

    /**
     * Set the value of the Field. This method is responsible
     * for modifying the internal state of the field and applying the
     * set filters.
     *
     * @param mixed $value The value to be set
     */
    public function setValue($value);

    /**
     * Get the internal state of the Field. The internal state
     * is a bit mask of STATE_NEW and STATE_DIRTY. If neither
     * is set, the internal state equals STATE_CLEAN.
     *
     * @return integer The current state of the Field
     */
    public function getState();

    /**
     * Set the internal state of the Field. The internal state
     * is a bit mask of STATE_NEW and STATE_DIRTY. If neither
     * is set, the internal state equals STATE_CLEAN.
     *
     * @param integer $state
     */
    public function setState($state);

    /**
     * Add a filter which is applied to the value before it is actually set.
     * These filters can be numeric-string conversions and/or whitespace trimming.
     *
     * All set filters will be applied sequentially.
     *
     * @param Closure $filter The filter to add to the filter sequence
     *
     * @return Field
     */
    public function addFilter(Closure $filter);

    /**
     *
     * Get the dehydrated form of the Field, suitable for Mongo document storage
     *
     * @return array
     */
    public function dehydrate();

    /**
     * Accept the dehydrated form to
     * set this Field's content.
     *
     * @param $value The dehydrated form of the Field
     */
    public function hydrate($value);

    /**
     * Set the Field's state to clean
     *
     */
    public function clean();

    /**
     * Return the mutations which are required to execute
     * in order to store any modifications in this Field (or
     * underlying Fields) in the Mongo document.
     *
     * @param $path The path preceding this Field in the document
     * @param $name The name by which the Field is known in its container
     *
     * @return array The collection of mutations in the Field
     */
    public function getMutations($path = null, $name = null);

    /**
     *
     * Rewrite a portion of a query concerning this field to
     * project the query properties.
     *
     * @param $partialQuery
     * @return array
     */
    public function rewriteQuery(array $partialQuery);

}

/**
 * CompositeField denotes a Field which is composed of (optionally named) sub Fields.
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @author Gido Hakvoort <gido@hakvoort.it>
 *
 */
interface CompositeField {

    /**
     *
     * Check whether the CompositeField has a Field with the given name.
     *
     * @param $name The name of the Field to check
     *
     * @return boolean True when the record has a Field of this name
     */
    public function hasField($name);

    /**
     * Get the Field with the given name.
     *
     * @param string $name
     *
     * @return Field|null
     */
    public function getField($name);
}

/**
 * The base class of most Fields, implementing the most
 * basic useful methods.
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @author Gido Hakvoort <gido@hakvoort.it>
 *
 */
abstract class AbstractField implements Field {

    protected $_state;
    protected $_filters = array();

    /**
     * Define a field as new
     */
    public function __construct() {
        $this->_state = self :: STATE_NEW;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.Field::setValue()
     */
    public function setValue($value) {
        $value = $this->filter($value);

        if($this->setValueImpl($value)) {
            $this->_state |= self :: STATE_DIRTY;
        }
    }

    /**
     *
     * Set the value, without needing to filter it or change any
     * additional state besides the value.
     *
     * @param mixed $value
     *
     * @return boolean True if the Field's internal value has changed, false otherwise
     */
    protected abstract function setValueImpl($value);

    /**
     * (non-PHPdoc)
     * @see src/mongrove.Field::getState()
     */
    public function getState() {
        return $this->_state;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.Field::setState()
     */
    public function setState($state) {
        $this->_state = $state;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.Field::addFilter()
     */
    public function addFilter(Closure $filter) {
        $this->_filters[] = $filter;
    }

    /**
     * Apply all filters to the value
     *
     * @param mixed $value The value to be filtered
     * @return mixed The filtered value
     */
    protected function filter($value) {
        foreach($this->_filters as $filter) {
            $value = $filter($value);
        }
        return $value;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.Field::clean()
     */
    public function clean() {
        $this->_state = self :: STATE_CLEAN;
    }

    /**
     *
     * @return boolean True if the Field is newly created
     */
    protected function isNew() {
        return ($this->_state & self :: STATE_NEW) === self :: STATE_NEW;
    }

    /**
     *
     * @return boolean True if the Field is modified
     */
    protected function isModified() {
        return ($this->_state & self :: STATE_DIRTY) === self :: STATE_DIRTY;
    }
}

/**
 *
 * SimpleField is the most simple representation of any value that can be
 * stored in a Mongo document. It imposes no restriction and/or checks on
 * the internally stored values. As it imposes no restrictions or checks it
 * is considered unsafe and unfavorable for direct usage.
 *
 * @author Gido Hakvoort <gido@hakvoort.it>
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 *
 */
class SimpleField extends AbstractField {

    protected $value;

    /**
     * (non-PHPdoc)
     * @see src/mongrove.Field::getValue()
     */
    public function getValue() {
        return $this->value;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.AbstractField::setValueImpl()
     */
    protected function setValueImpl($value) {
        if($this->value === $value) {
            return false;
        }

        $this->value = $value;

        return true;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.Field::hydrate()
     */
    public function hydrate($value) {
        $this->value = $value;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.Field::dehydrate()
     */
    public function dehydrate() {
        return $this->value;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.Field::getMutations()
     */
    public function getMutations($path = null, $name = null) {
        $mutations = array();

        if($this->isModified()) {
            $path === null ?: $path .= '.';
            $mutations[] = array(Command :: OP_SET => array("{$path}{$name}" => $this->value));
        }

        return $mutations;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.Field::rewriteQuery()
     */
    public function rewriteQuery(array $partialQuery) {
        /*
         * Do not rewrite by default
         */
        return $partialQuery;
    }
}

/**
 * An abstraction for fields requiring atomic modifications
 *
 * @author Gido Hakvoort <gido@hakvoort.it>
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 *
 */
abstract class AbstractAtomicField extends AbstractField {

}